package jwt

import (
	"errors"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
	"github.com/redis/go-redis/v9"
	"strings"
	"time"
)

// RedisJwtHandler 基于 Redis 的实现
type RedisJwtHandler struct {
	signingMethod jwt.SigningMethod
	rcExpiration  time.Duration // 长token的过期时间
	cmd           redis.Cmdable
}

func NewRedisJwtHandler(cmd redis.Cmdable) Handler {
	return &RedisJwtHandler{
		cmd:           cmd,
		signingMethod: jwt.SigningMethodHS512,
		rcExpiration:  time.Hour * 24 * 7,
	}
}

// UserClaims 这个Claims就是你要塞入jwt里的数据
type UserClaims struct {
	jwt.RegisteredClaims // 这个实现了 Claims 接口
	UserId               int64
	UserAgent            string // 用于标识浏览器信息，加强登录安全性
	Ssid                 string // 标识本次登录，如果ssid在Redis->登录已失效
}

// RefreshClaims 长token
type RefreshClaims struct {
	jwt.RegisteredClaims
	UserId int64
	Ssid   string // 标识本次登录，如果ssid在Redis->登录已失效
}

// JWTKey
// 这个key的话 不同的SigningMethod对应不同要求，可以看源码
// 这个key建议跟之前的一样去网上生成密钥
var JWTKey = []byte("Xw4Gxn3nwFTyG9PM1YUzhfEB27bEnntb")
var RefreshJWTKey = []byte("Xw4Gxn3nwFTyG9PM1YUzhfEB27bEnnDI")

// ExtractToken 从 authorization 中提取 token
// Bearer xxx
func (h *RedisJwtHandler) ExtractToken(ctx *gin.Context) string {
	authCode := ctx.GetHeader("Authorization")
	if authCode == "" { // 没有登录
		return ""
	}
	segments := strings.Split(authCode, " ")
	if len(segments) != 2 { // 乱传的authorization
		return ""
	}
	return segments[1]
}

// SetLoginToken 抽取出来一个单独的方法，作为登录后设置 token 的统一逻辑
func (h *RedisJwtHandler) SetLoginToken(ctx *gin.Context, uid int64) error {
	ssid := uuid.New().String()
	err := h.SetJWTToken(ctx, uid, ssid)
	if err != nil {
		return err
	}
	return h.setRefreshToken(ctx, uid, ssid)
}

// SetJWTToken 生成JWT token 并设置到 Header 中
func (h *RedisJwtHandler) SetJWTToken(ctx *gin.Context, uid int64, ssid string) error {
	userClaims := UserClaims{
		UserId:    uid,
		UserAgent: ctx.GetHeader("User-Agent"),
		Ssid:      ssid,
		RegisteredClaims: jwt.RegisteredClaims{
			// 设置过期时间
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Minute * 30)),
		},
	}
	// 这个 SigningMethod，虽然之间有性能差异，但是一般随便选就可以
	token := jwt.NewWithClaims(h.signingMethod, userClaims)
	tokenStr, err := token.SignedString(JWTKey)
	if err != nil { // 只有你传入参数类型不对的时候，会有这个err
		return err
	}
	ctx.Header("x-jwt-token", tokenStr)
	return nil
}

// 设置长token , x-refresh-token
func (h *RedisJwtHandler) setRefreshToken(ctx *gin.Context, uid int64, ssid string) error {
	refreshClaims := RefreshClaims{
		UserId: uid,
		Ssid:   ssid,
		RegisteredClaims: jwt.RegisteredClaims{
			// 设置过期时间
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(h.rcExpiration)),
		},
	}
	// 这个 SigningMethod，虽然之间有性能差异，但是一般随便选就可以
	refreshToken := jwt.NewWithClaims(h.signingMethod, refreshClaims)
	refreshTokenStr, err := refreshToken.SignedString(RefreshJWTKey)
	if err != nil { // 只有你传入参数类型不对的时候，会有这个err
		return err
	}
	ctx.Header("x-refresh-token", refreshTokenStr)
	return nil
}

// ClearToken 清除jwt并将 ssid 保存到Redis中
func (h *RedisJwtHandler) ClearToken(ctx *gin.Context) error {
	ctx.Header("x-refresh-token", "")
	ctx.Header("x-jwt-token", "")
	userClaims := ctx.MustGet("user").(UserClaims)
	return h.cmd.Set(ctx, fmt.Sprintf("users:ssid:%s", userClaims.Ssid),
		"", h.rcExpiration).Err()
}

// CheckSession 检查 ssid 是否有效
func (h *RedisJwtHandler) CheckSession(ctx *gin.Context, ssid string) error {
	cnt, err := h.cmd.Exists(ctx, fmt.Sprintf("users:ssid:%s", ssid)).Result()
	if err != nil {
		return err
	}
	if cnt > 0 {
		return errors.New("token已失效")
	}
	return nil
}
